#!/bin/zsh
set -euo pipefail

PORT=4180
APP_VERSION="v60.1"
APP_URL="http://127.0.0.1:${PORT}/"
APP_ROOT="${0:A:h:h}"
APP_HTML="${APP_ROOT}/Resources/arcadia-ccm-simulator-v60.1.html"
BUNDLED_NODE="${APP_ROOT}/Resources/bin/node"

if curl -fsS "http://127.0.0.1:${PORT}/health" >/dev/null 2>&1; then
  open "${APP_URL}"
  exit 0
fi

if [[ ! -x "${BUNDLED_NODE}" ]]; then
  osascript -e 'display alert "Arcadia CCM Simulator" message "The bundled runtime is missing from this app." as critical'
  exit 1
fi

export ARCADIA_APP_HTML="${APP_HTML}"
export ARCADIA_APP_VERSION="${APP_VERSION}"

"${BUNDLED_NODE}" <<'NODE_SERVER'
const http = require('node:http');
const fs = require('node:fs');
const { execFile } = require('node:child_process');

const PORT = 4180;
const APP_HTML = fs.readFileSync(process.env.ARCADIA_APP_HTML, 'utf8');
const APP_VERSION = process.env.ARCADIA_APP_VERSION || 'v60.1';
const tokenCache = new Map();
const ENDPOINTS = {
  devices: '/api/2/devices',
  deviceCapability: '/api/1/devices/0/capability',
  linkGroupCapability: '/api/1/capabilities/linkgroup',
  connections: '/api/1/connections',
  connectionLiveStatus: '/api/1/connections/liveStatus',
  endpoints: '/api/1/devices/endpoints',
  interfacePorts: '/api/1/devices/interfaces/ports',
  interfaces: '/api/1/devices/interfaces',
  keysets: '/api/2/keysets',
  rolesets: '/api/2/rolesets',
  users: '/api/2/users',
  ivpUsers: '/api/1/ivpusers',
  gpio: '/api/1/devices/0/gpio',
  deviceCapabilities: '/api/1/capabilities/devices',
  resourceAllocationOptions: '/api/1/resourceAllocationOptions',
  dataDefinition: '/api/1/datadefinition',
  deviceSetupDefinition: '/api/1/datadefinition/device/setupdatadefinition',
  endpointSetupDefinition: '/api/1/datadefinition/endpoint/setupdatadefinition',
  ccmUsers: '/api/1/ccmusers',
  externalDevices: '/api/1/externalDevices'
};

const server = http.createServer(async (req, res) => {
  addCors(res);
  if (req.method === 'OPTIONS') return endStatus(res, 204);
  const requestUrl = new URL(req.url, `http://${req.headers.host}`);

  if (req.method === 'GET' && requestUrl.pathname === '/health') {
    return sendJson(res, 200, { ok: true, service: 'Arcadia CCM Simulator bundled app', version: APP_VERSION, port: PORT });
  }

  if (req.method === 'GET' && (requestUrl.pathname === '/' || requestUrl.pathname === '/arcadia-ccm-simulator.html')) {
    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });
    res.end(APP_HTML);
    return;
  }

  if (req.method !== 'GET' || requestUrl.pathname !== '/live-read') {
    return sendJson(res, 404, { ok: false, message: 'Use / for the simulator or /live-read?ip=192.168.1.75 for API reads' });
  }

  const ip = normalizeIp(requestUrl.searchParams.get('ip'));
  const username = requestUrl.searchParams.get('username') || '';
  const password = requestUrl.searchParams.get('password') || '';
  let token = requestUrl.searchParams.get('token') || tokenCache.get(ip) || '';
  if (!ip) return sendJson(res, 400, { ok: false, message: 'Missing Arcadia IP' });

  try {
    let login = { attempted: false, ok: false };
    const liveApi = await readAllArcadiaEndpoints(ip, token);
    sendJson(res, 200, { ok: true, ip, login, liveApi });
  } catch (error) {
    if (needsAuth(error) && username && password) {
      try {
        tokenCache.delete(ip);
        const loginResult = await loginArcadia(ip, username, password);
        token = loginResult.token;
        if (token) tokenCache.set(ip, token);
        const liveApi = await readAllArcadiaEndpoints(ip, token);
        return sendJson(res, 200, { ok: true, ip, login: { attempted: true, ok: Boolean(token), status: loginResult.status }, liveApi });
      } catch (retryError) {
        return sendJson(res, 502, { ok: false, message: retryError.message || String(retryError) });
      }
    }
    sendJson(res, 502, { ok: false, message: error.message || String(error) });
  }
});

server.listen(PORT, '127.0.0.1', () => {
  const url = `http://127.0.0.1:${PORT}/`;
  console.log(`Arcadia CCM Simulator ${APP_VERSION} listening at ${url}`);
  execFile('open', [url], () => {});
});

function normalizeIp(value) {
  return String(value || '').trim().replace(/^https?:\/\//, '').replace(/\/.*$/, '');
}

async function loginArcadia(ip, username, password) {
  const body = new URLSearchParams({ logemail: username, logpassword: password, origin: '/view/status/overview' });
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 6500);
  try {
    const response = await fetch(`http://${ip}/auth/local/login`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body,
      redirect: 'manual',
      signal: controller.signal
    });
    const token = response.headers.get('x-jwt') || '';
    if (!token) throw new Error(`Login did not return a JWT (${response.status})`);
    return { status: response.status, token };
  } catch (error) {
    throw new Error(error.name === 'AbortError' ? 'Timed out logging into Arcadia' : (error.message || String(error)));
  } finally {
    clearTimeout(timeoutId);
  }
}

async function readArcadiaEndpoint(ip, apiPath, token, required = false) {
  const headers = { Accept: 'application/json' };
  if (token) {
    headers.Authorization = token.startsWith('Bearer ') ? token : `Bearer ${token}`;
    headers['X-Access-Token'] = token;
    headers.token = token;
  }
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), 6500);
  try {
    const response = await fetch(`http://${ip}${apiPath}`, { method: 'GET', headers, signal: controller.signal });
    const text = await response.text();
    const data = parseJson(text);
    if (!response.ok || data?.ok === false) {
      const message = data?.message || `${response.status} ${response.statusText}`;
      if (required) throw new Error(message);
      return { error: message, status: response.status, data };
    }
    return data;
  } catch (error) {
    const message = error.name === 'AbortError' ? 'Timed out waiting for Arcadia API' : (error.message || String(error));
    if (required) throw new Error(message);
    return { error: message };
  } finally {
    clearTimeout(timeoutId);
  }
}

async function readAllArcadiaEndpoints(ip, token) {
  const liveApi = {};
  for (const [key, apiPath] of Object.entries(ENDPOINTS)) {
    liveApi[key] = await readArcadiaEndpoint(ip, apiPath, token, key === 'devices');
  }
  return liveApi;
}

function parseJson(text) {
  try { return text ? JSON.parse(text) : null; } catch { return { raw: text }; }
}

function needsAuth(error) {
  const message = String(error?.message || error || '');
  return /invalid token|unauthorized|forbidden|jwt|401|403/i.test(message);
}

function addCors(res) {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Accept, Authorization, X-Access-Token, token');
}

function sendJson(res, status, payload) {
  res.writeHead(status, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(payload, null, 2));
}

function endStatus(res, status) {
  res.writeHead(status);
  res.end();
}
NODE_SERVER
